/*
 * Copyright (C) 2008 Peter Kling
 * 
 * This file is part of 'A Kind of Billiard', a graphical billiard simulation.
 * 
 * 'A Kind of Billiard' is free software: you can redistribute it and/or modify it under the terms of the GNU General
 * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 * 
 * 'A Kind of Billiard' is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with 'A Kind of Billiard'. If not, see
 * <http://www.gnu.org/licenses/>.
 */

package de.akob.shadow;

import javax.media.opengl.GL;

import de.akob.misc.OpenGLDrawable;
import de.akob.shader.Shader;

/**
 * Implementation of volume shadowing. This class will render any class implementing the OpenGLDrawable interface using
 * volume shadowing. It can be used as a wrapper by constructing it with the OpenGLDrawable that is to be drawn with
 * shadows and calling the draw method of the shadower afterwards.
 * 
 * Note: Shadowers do currently only draw shadows for GL_LIGHT0.
 */
public class Shadower implements OpenGLDrawable {
	// shaders used for volume shadowing
	public static final String VERTEXSHADER_SHADOW     = "shaders/shadowVolume_vp.cg";
	public static final String FRAGMENTSHADER_SHADOW   = "shaders/shadowVolume_fp.cg";
	public static final String VERTEXSHADER_LIGHTING   = "shaders/phongPerPixel_vp.cg";
	public static final String FRAGMENTSHADER_LIGHTING = "shaders/phongPerPixel_fp.cg";
	
	// the shadow volume shader and the lighting shader
	private Shader             shader_shadowVolume     = null;
	private Shader             shader_lighting         = null;
	
	// the object that will be drawn with volume shadows
	private OpenGLDrawable     drawableObject;
	
	/**
	 * Constructs a shadower for the given drawable object.
	 */
	public Shadower(OpenGLDrawable drawableObject) {
		this.drawableObject = drawableObject;
	}
	
	/**
	 * Draws the object this shadower was created for with volume shadowing. Note that after a call to this method, the
	 * standard OpenGL shader will be in use.
	 */
	public void draw(GL gl) {
		storeOpenGLState(gl);
		
		setupShadowingPhase1(gl);
		drawableObject.draw(gl);
		
		setupShadowingPhase2(gl);
		drawableObject.draw(gl);
		
		setupShadowingPhase3(gl);
		drawableObject.draw(gl);
		
		setupShadowingPhase4(gl);
		drawableObject.draw(gl);
		
		restoreOpenGLState(gl);
	}
	
	/**
	 * Draws the object with its shadow volume used for volume shadowing.
	 */
	public void draw_shadowVolume(GL gl) {
		gl.glEnable(GL.GL_CULL_FACE);
		gl.glCullFace(GL.GL_BACK);
		drawableObject.draw(gl);
		shader_shadowVolume.enable();
		drawableObject.draw(gl);
		shader_shadowVolume.disable();
	}
	
	/**
	 * Initializes this object.
	 */
	public void init(GL gl) {
		shader_shadowVolume = new Shader(VERTEXSHADER_SHADOW, FRAGMENTSHADER_SHADOW);
		shader_lighting = new Shader(VERTEXSHADER_LIGHTING, FRAGMENTSHADER_LIGHTING);
	}
	
	/**
	 * Restores the part of the OpenGL state used for volume shadowing.
	 */
	private void restoreOpenGLState(GL gl) {
		gl.glPopAttrib();
		shader_lighting.disable();
	}
	
	/**
	 * Setup of OpenGL context for phase 1 of shadowing. The first phase will render the scene without updating the
	 * color buffer (while the depth buffer is still updated). Call context: phase 1 --> draw obj. --> phase 2 --> draw
	 * obj.--> phase 3 --> draw obj. phase 4 --> draw obj.
	 */
	private void setupShadowingPhase1(GL gl) {
		shader_lighting.enable();
		shader_lighting.setFragmentParameter("lightEnabled", 0);
	}
	
	/**
	 * Setup of OpenGL context for phase 2 of shadowing. The second phase will count how many back faces of shadow
	 * volumes are penetrated by a ray from the eye.
	 * <p>
	 * Call context: phase 1 --> draw obj. --> phase 2 --> draw obj.--> phase 3 --> draw obj. phase 4 --> draw obj.
	 */
	private void setupShadowingPhase2(GL gl) {
		// enable shadow volume shader
		shader_shadowVolume.enable();
		
		// enable stencil test and make sure that stencil buffer is cleared
		gl.glEnable(GL.GL_STENCIL_TEST);
		gl.glClear(GL.GL_STENCIL_BUFFER_BIT);
		
		// enable culling
		gl.glEnable(GL.GL_CULL_FACE);
		
		// disable writing depth/color buffers
		gl.glColorMask(false, false, false, false);
		gl.glDepthMask(false);
		
		// cull front faces
		gl.glCullFace(GL.GL_FRONT);
		
		// setup stencil test to count back faces penetrated by eye-ray.
		gl.glStencilFunc(GL.GL_ALWAYS, 0x0, ~0x0);
		gl.glStencilOp(GL.GL_KEEP, GL.GL_INCR, GL.GL_KEEP);
	}
	
	/**
	 * Setup of OpenGL context for phase 3 of shadowing. The third phase will count how many front faces of shadow
	 * volumes are penetrated by a ray from the eye.
	 * <p>
	 * Call context: phase 1 --> draw obj. --> phase 2 --> draw obj.--> phase 3 --> draw obj. phase 4 --> draw obj.
	 */
	private void setupShadowingPhase3(GL gl) {
		// cull back faces
		gl.glCullFace(GL.GL_BACK);
		
		// setup stencil test to count front faces penetrated by eye-ray.
		gl.glStencilFunc(GL.GL_ALWAYS, 0x0, ~0x0);
		gl.glStencilOp(GL.GL_KEEP, GL.GL_DECR, GL.GL_KEEP);
	}
	
	/**
	 * Setup of OpenGL context for phase 4 of shadowing. The fourth phase will render only the lit elements of the
	 * scene.
	 * <p>
	 * Call context: phase 1 --> draw obj. --> phase 2 --> draw obj.--> phase 3 --> draw obj. phase 4 --> draw obj.
	 */
	private void setupShadowingPhase4(GL gl) {
		// re-enable light
		shader_shadowVolume.disable();
		shader_lighting.enable();
		shader_lighting.setFragmentParameter("lightEnabled", 1);
		
		// enable writing in color and depth buffers
		gl.glColorMask(true, true, true, true);
		gl.glDepthMask(true);
		
		// draw pixels according to stencil buffer (and depth test)
		gl.glDepthFunc(GL.GL_EQUAL);
		gl.glStencilFunc(GL.GL_EQUAL, 0x0, ~0x0);
		gl.glStencilOp(GL.GL_KEEP, GL.GL_KEEP, GL.GL_KEEP);
	}
	
	/**
	 * Stores the parts of the OpenGL state used for volume shadowing.
	 */
	public void storeOpenGLState(GL gl) {
		gl.glPushAttrib(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT | GL.GL_STENCIL_BUFFER_BIT | GL.GL_POLYGON_BIT);
	}
}
